﻿___TERMS_OF_SERVICE___

By creating or modifying this file you agree to Google Tag Manager's Community
Template Gallery Developer Terms of Service available at
https://developers.google.com/tag-manager/gallery-tos (or such other URL as
Google may provide), as modified from time to time.


___INFO___

{
  "type": "TAG",
  "id": "cvt_temp_public_id",
  "version": 1,
  "securityGroups": [],
  "displayName": "TikTok Pixel",
  "categories": [
    "ADVERTISING",
    "ANALYTICS"
  ],
  "brand": {
    "id": "github.com_tiktok",
    "displayName": "tiktok",
    "thumbnail": "\u003d\u003d"
  },
  "description": "Install TikTok Pixel easily to your site without the need to write a line of code.",
  "containerContexts": [
    "WEB"
  ]
}


___TEMPLATE_PARAMETERS___

[
  {
    "type": "GROUP",
    "name": "Section 1 - TikTok Event Configuration",
    "displayName": "TikTok Event Configuration",
    "groupStyle": "NO_ZIPPY",
    "subParams": [
      {
        "type": "TEXT",
        "name": "pixel_code",
        "displayName": "Pixel ID",
        "simpleValueType": true,
        "valueValidators": [],
        "help": "You can find your Pixel ID in Events Manager",
        "valueHint": "CD9079RC77U0N3GBV16Y"
      },
      {
        "type": "SELECT",
        "name": "event",
        "displayName": "Event Name",
        "macrosInSelect": true,
        "selectItems": [
          {
            "value": "CompletePayment",
            "displayValue": "CompletePayment"
          },
          {
            "value": "AddToCart",
            "displayValue": "AddToCart"
          },
          {
            "value": "ViewContent",
            "displayValue": "ViewContent"
          },
          {
            "value": "InitiateCheckout",
            "displayValue": "InitiateCheckout"
          },
          {
            "value": "AddToWishlist",
            "displayValue": "AddToWishlist"
          },
          {
            "value": "AddPaymentInfo",
            "displayValue": "AddPaymentInfo"
          },
          {
            "value": "PlaceAnOrder",
            "displayValue": "PlaceAnOrder"
          },
          {
            "value": "Contact",
            "displayValue": "Contact"
          },
          {
            "value": "Download",
            "displayValue": "Download"
          },
          {
            "value": "SubmitForm",
            "displayValue": "SubmitForm"
          },
          {
            "value": "CompleteRegistration",
            "displayValue": "CompleteRegistration"
          },
          {
            "value": "Subscribe",
            "displayValue": "Subscribe"
          },
          {
            "value": "ClickButton",
            "displayValue": "ClickButton"
          },
          {
            "value": "Search",
            "displayValue": "Search"
          }
        ],
        "simpleValueType": true,
        "help": "TikTok supports 14 standard events. You may refer to our \u003ca href\u003d\"https://ads.tiktok.com/help/article?aid\u003d10028\" target\u003d\"_blank\"\u003edocumentation\u003c/a\u003e and choose a suitable event to tag.",
        "defaultValue": "{{Event}}"
      }
    ],
    "help": "\u003ca href\u003d\"https://business-api.tiktok.com/portal/docs?id\u003d1799004006287425\" target\u003d\"_blank\"\u003eLearn more\u003c/a\u003e about using TikTok Pixel with Google Tag Manager."
  },
  {
    "type": "GROUP",
    "name": "Section 3 - Parameter Override",
    "displayName": "Parameter Override",
    "groupStyle": "ZIPPY_CLOSED",
    "subParams": [
      {
        "type": "LABEL",
        "name": "Parameter Override Description",
        "displayName": "\u003ca target\u003d\"_blank\" href\u003d\"https://business-api.tiktok.com/portal/docs?id\u003d1799004110681154#item-link-How%20to%20set%20up%20deduplication\"\u003eLearn more\u003c/a\u003e about how to manually configure event parameters with \"Parameter Override\"."
      },
      {
        "type": "GROUP",
        "name": "Advanced Matching Configurations",
        "displayName": "Advanced Matching Configurations",
        "groupStyle": "NO_ZIPPY",
        "subParams": [
          {
            "type": "RADIO",
            "name": "hash",
            "displayName": "Hashed / Non-Hashed AM Parameters",
            "radioItems": [
              {
                "value": "hashed",
                "displayValue": "SHA256 AM Parameters"
              },
              {
                "value": "non-hashed",
                "displayValue": "Plain AM Parameters",
                "help": "TikTok Pixel will hash plain email and phone for you"
              }
            ],
            "simpleValueType": true,
            "help": "Tell us if your AM parameters have been hashed."
          },
          {
            "type": "TEXT",
            "name": "email",
            "displayName": "Email",
            "simpleValueType": true,
            "enablingConditions": [
              {
                "paramName": "hash",
                "paramValue": "non-hashed",
                "type": "EQUALS"
              }
            ]
          },
          {
            "type": "TEXT",
            "name": "phone",
            "displayName": "Phone",
            "simpleValueType": true,
            "enablingConditions": [
              {
                "paramName": "hash",
                "paramValue": "non-hashed",
                "type": "EQUALS"
              }
            ],
            "help": "If the country code is 86, then do not include country code (example: 13800000000). Otherwise, include country code with + and remove any other characters (spaces, \u0027-\u0027) between numbers (example for US: +12133734253)."
          },
          {
            "type": "TEXT",
            "name": "external_id",
            "displayName": "External ID",
            "simpleValueType": true,
            "enablingConditions": [
              {
                "paramName": "hash",
                "paramValue": "non-hashed",
                "type": "EQUALS"
              }
            ],
            "help": "Any unique identifier on the advertiser\u0027s side, such as loyalty membership ID, user ID, or external cookie ID."
          },
          {
            "type": "TEXT",
            "name": "sha256_email",
            "displayName": "SHA256 Hashed Email",
            "simpleValueType": true,
            "enablingConditions": [
              {
                "paramName": "hash",
                "paramValue": "hashed",
                "type": "EQUALS"
              }
            ]
          },
          {
            "type": "TEXT",
            "name": "sha256_phone",
            "displayName": "SHA256 Hashed Phone",
            "simpleValueType": true,
            "enablingConditions": [
              {
                "paramName": "hash",
                "paramValue": "hashed",
                "type": "EQUALS"
              }
            ],
            "help": "If the country code is 86, then do not include country code (example: 13800000000). Otherwise, include country code with + and remove any other characters (spaces, \u0027-\u0027) between numbers (example for US: +12133734253)."
          },
          {
            "type": "TEXT",
            "name": "sha256_external_id",
            "displayName": "SHA256 Hashed External ID",
            "simpleValueType": true,
            "enablingConditions": [
              {
                "paramName": "hash",
                "paramValue": "hashed",
                "type": "EQUALS"
              }
            ],
            "help": "Any unique identifier on the advertiser\u0027s side, such as loyalty membership ID, user ID, or external cookie ID."
          }
        ],
        "help": "Advance matching parameters are hashed and then shared with TikTok to help attribute more conversions to your ads and improve ad retargeting. \u003cbr\u003e \u003ca target\u003d\"_blank\" href\u003d\"https://business-api.tiktok.com/portal/docs?id\u003d1799004110681154#item-link-User%20data\"\u003eLearn more\u003c/a\u003e about how to format Advanced Matching parameters."
      },
      {
        "type": "GROUP",
        "name": "Properties",
        "displayName": "Properties (e.g. product data)",
        "groupStyle": "NO_ZIPPY",
        "subParams": [
          {
            "type": "CHECKBOX",
            "name": "enhance_ecomm",
            "checkboxText": "Use Google Analytics E-commerce Data Layers (Recommended)",
            "simpleValueType": true,
            "help": "If you have Google\u0027s \u003ca href\u003d\"https://developers.google.com/analytics/devguides/collection/ga4/ecommerce?client_type\u003dgtm#implementation\" target\u003d\"_blank\"\u003eEnhanced E-Commerce\u003c/a\u003e or \u003ca href\u003d\"https://support.google.com/tagmanager/answer/6107169?hl\u003den#standard-ecommerce\" target\u003d\"_blank\"\u003e Standard E-Commerce\u003c/a\u003e Data Layers set up to collect information about the shopping behavior of your users. TikTok will automatically collect these data as parameters to help you improve your ad performance.",
            "defaultValue": true
          },
          {
            "type": "RADIO",
            "name": "ga_ecomm",
            "displayName": "Google Analytics E-commerce Data Layers",
            "radioItems": [
              {
                "value": "ecomm",
                "displayValue": "Standard Ecommerce"
              },
              {
                "value": "enhance_ecomm",
                "displayValue": "Enhanced Ecommerce"
              }
            ],
            "simpleValueType": true,
            "enablingConditions": [
              {
                "paramName": "enhance_ecomm",
                "paramValue": true,
                "type": "EQUALS"
              }
            ],
            "defaultValue": "ecomm"
          },
          {
            "type": "TEXT",
            "name": "ecomm_currency",
            "displayName": "Currency",
            "simpleValueType": true,
            "valueHint": "USD",
            "help": "If you haven\u0027t added the currency to the e-commerce data layer, add the current code here. Note: Adding the code will replace the \u0027currency code\u0027 or \u0027currency\u0027 field from your data layer.",
            "enablingConditions": [
              {
                "paramName": "enhance_ecomm",
                "paramValue": true,
                "type": "EQUALS"
              }
            ]
          },
          {
            "type": "RADIO",
            "name": "single_multi_product",
            "displayName": "Manually Input Single / Multiple Products from Data Layer",
            "radioItems": [
              {
                "value": "single",
                "displayValue": "Single Content"
              },
              {
                "value": "multiple",
                "displayValue": "Multiple Contents",
                "help": "Send a list of contents as a JSON string. \u003ca href\u003d\"https://developer.mozilla.org/en-US/docs/Learn/JavaScript/Objects/JSON\" target\u003d\"_blank\"\u003eLearn more about JSON\u003c/a\u003e"
              },
              {
                "value": "empty",
                "displayValue": "No Contents"
              }
            ],
            "simpleValueType": true,
            "enablingConditions": [
              {
                "paramName": "enhance_ecomm",
                "paramValue": false,
                "type": "EQUALS"
              }
            ]
          },
          {
            "type": "TEXT",
            "name": "contents",
            "displayName": "contents",
            "simpleValueType": true,
            "enablingConditions": [
              {
                "paramName": "single_multi_product",
                "paramValue": "multiple",
                "type": "EQUALS"
              }
            ],
            "help": "Please make sure \"contents\" is \u003ca href\u003d\"https://business-api.tiktok.com/portal/docs?id\u003d1799004110681154#item-link-contents%20parameters\" target\u003d\"_blank\"\u003ea list of valid content objects\u003c/a\u003e or encoded as \u003ca href\u003d\"https://developer.mozilla.org/en-US/docs/Learn/JavaScript/Objects/JSON\" target\u003d\"_blank\"\u003ea valid JSON string\u003c/a\u003e.",
            "valueHint": "[{\"content_id\": \"abc\", \"price\": 10, \"quantity\": 1}]"
          },
          {
            "type": "TEXT",
            "name": "content_id",
            "displayName": "content_id",
            "simpleValueType": true,
            "enablingConditions": [
              {
                "paramName": "single_multi_product",
                "paramValue": "single",
                "type": "EQUALS"
              }
            ],
            "help": "Recommended: Unique ID of the item, either SKU ID or item group ID"
          },
          {
            "type": "SELECT",
            "name": "content_type",
            "displayName": "content_type",
            "macrosInSelect": false,
            "selectItems": [
              {
                "value": "product",
                "displayValue": "product"
              },
              {
                "value": "product_group",
                "displayValue": "product_group"
              }
            ],
            "simpleValueType": true,
            "enablingConditions": [
              {
                "paramName": "single_multi_product",
                "paramValue": "single",
                "type": "EQUALS"
              }
            ],
            "help": "Recommended: Select \"product\" if your content_id is SKU ID, or select \"product_group\" if your content_id is item group ID",
            "notSetText": ""
          },
          {
            "type": "TEXT",
            "name": "content_name",
            "displayName": "content_name",
            "simpleValueType": true,
            "enablingConditions": [
              {
                "paramName": "single_multi_product",
                "paramValue": "single",
                "type": "EQUALS"
              }
            ],
            "help": "Recommended: Title of your product or page"
          },
          {
            "type": "TEXT",
            "name": "content_category",
            "displayName": "content_category",
            "simpleValueType": true,
            "help": "Recommended: Category of the product or page",
            "enablingConditions": [
              {
                "paramName": "single_multi_product",
                "paramValue": "single",
                "type": "EQUALS"
              }
            ]
          },
          {
            "type": "TEXT",
            "name": "price",
            "displayName": "price",
            "simpleValueType": true,
            "enablingConditions": [
              {
                "paramName": "single_multi_product",
                "paramValue": "single",
                "type": "EQUALS"
              }
            ],
            "help": "Recommended: Price of this item"
          },
          {
            "type": "TEXT",
            "name": "quantity",
            "displayName": "quantity",
            "simpleValueType": true,
            "enablingConditions": [
              {
                "paramName": "single_multi_product",
                "paramValue": "single",
                "type": "EQUALS"
              }
            ],
            "help": "Recommended: Quantity of this item"
          },
          {
            "type": "TEXT",
            "name": "value",
            "displayName": "value",
            "simpleValueType": true,
            "enablingConditions": [
              {
                "paramName": "enhance_ecomm",
                "paramValue": false,
                "type": "EQUALS"
              }
            ],
            "help": "Recommended: Total value of the order"
          },
          {
            "type": "TEXT",
            "name": "currency",
            "displayName": "currency",
            "simpleValueType": true,
            "enablingConditions": [
              {
                "paramName": "enhance_ecomm",
                "paramValue": false,
                "type": "EQUALS"
              }
            ],
            "help": "Recommended: ISO 4217 code. Examples: \"EUR\", \"USD\", \"JPY\". See \u003ca href\u003d\"https://ads.tiktok.com/help/article/about-parameters?lang\u003den\" target\u003d\"_blank\"\u003ehere\u003c/a\u003e for a list of currencies supported."
          },
          {
            "type": "TEXT",
            "name": "description",
            "displayName": "description",
            "simpleValueType": true,
            "help": "Recommended: Description of the product or page",
            "enablingConditions": [
              {
                "paramName": "enhance_ecomm",
                "paramValue": false,
                "type": "EQUALS"
              }
            ]
          },
          {
            "type": "TEXT",
            "name": "brand",
            "displayName": "brand",
            "simpleValueType": true,
            "help": "Recommended: Brand name of the product",
            "enablingConditions": [
              {
                "paramName": "enhance_ecomm",
                "paramValue": false,
                "type": "EQUALS"
              }
            ]
          },
          {
            "type": "TEXT",
            "name": "query",
            "displayName": "query",
            "simpleValueType": true,
            "help": "Recommended for Search events: The text string that was searched for. For instance, a person searches for a product on your website, you can forward the keyword being searched here.",
            "enablingConditions": [
              {
                "paramName": "enhance_ecomm",
                "paramValue": false,
                "type": "EQUALS"
              }
            ]
          },
          {
            "type": "TEXT",
            "name": "status",
            "displayName": "status",
            "simpleValueType": true,
            "help": "Optional: Status of an order, item, or service.",
            "enablingConditions": [
              {
                "paramName": "enhance_ecomm",
                "paramValue": false,
                "type": "EQUALS"
              }
            ]
          }
        ],
        "help": "Configure and send \u003ca href\u003d\"https://business-api.tiktok.com/portal/docs?id\u003d1799004110681154#item-link-Product%20data\" target\u003d\"_blank\"\u003eadditional parameters\u003c/a\u003e to TikTok for to improve Ad performance and unlock Ad features."
      },
      {
        "type": "GROUP",
        "name": "De-duplication Configurations",
        "displayName": "De-duplication Configurations",
        "groupStyle": "NO_ZIPPY",
        "subParams": [
          {
            "type": "TEXT",
            "name": "event_id",
            "displayName": "Event ID",
            "simpleValueType": true
          }
        ],
        "help": "If you use both TikTok Pixel template and Events API template to send events, you can set up Event ID to deduplicate the two sources. \u003ca target\u003d\"_blank\" href\u003d\"https://business-api.tiktok.com/portal/docs?id\u003d1799004097478658#item-link-How%20to%20set%20up%20deduplication\"\u003eLearn more\u003c/a\u003e about how to set up deduplication."
      }
    ]
  },
  {
    "type": "GROUP",
    "name": "Section 2 - Additional Properties",
    "displayName": "Additional Properties",
    "groupStyle": "ZIPPY_CLOSED",
    "subParams": [
      {
        "type": "LABEL",
        "name": "Description",
        "displayName": "You may configure and send more custom parameters to TikTok to unlock additional Ad features targeted at specific verticals (i.e. Travel)"
      },
      {
        "type": "PARAM_TABLE",
        "name": "custom_properties",
        "displayName": "",
        "paramTableColumns": [
          {
            "param": {
              "type": "TEXT",
              "name": "key",
              "displayName": "Key",
              "simpleValueType": true,
              "canBeEmptyString": false
            },
            "isUnique": true
          },
          {
            "param": {
              "type": "TEXT",
              "name": "value",
              "displayName": "Value",
              "simpleValueType": true
            },
            "isUnique": false
          }
        ]
      }
    ]
  }
]


___SANDBOXED_JS_FOR_WEB_TEMPLATE___

/*
 * Copyright 2022 ByteDance Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

const version = "0_2_01";

const log = require("logToConsole");
const copyFromWindow = require("copyFromWindow");
const copyFromDataLayer = require("copyFromDataLayer");
const makeNumber = require("makeNumber");
const callInWindow = require("callInWindow");
const Object = require("Object");
const JSON = require("JSON");

const ecommerce = copyFromDataLayer("ecommerce");
const eventModel = copyFromDataLayer("eventModel");
const userDataFromDataLayer = copyFromDataLayer("user_data");
const ttContents = copyFromDataLayer('tt_contents') || (eventModel && eventModel.tt_contents);
const ttContentType = copyFromDataLayer('tt_content_type') || (eventModel && eventModel.tt_content_type);
const ttExternalId = copyFromDataLayer('tt_external_id') || (eventModel && eventModel.tt_external_id);

const ValidEvents = {
  ViewContent: 1,
  ClickButton: 1,
  Search: 1,
  AddToWishlist: 1,
  AddToCart: 1,
  InitiateCheckout: 1,
  AddPaymentInfo: 1,
  CompletePayment: 1,
  PlaceAnOrder: 1,
  Contact: 1,
  Download: 1,
  SubmitForm: 1,
  CompleteRegistration: 1,
  Subscribe: 1,
};

const TTEventMap = {
  'add_payment_info': 'AddPaymentInfo',
  'add_to_cart': 'AddToCart',
  'add_to_wishlist': 'AddToWishlist',
  'begin_checkout': 'InitiateCheckout',
  'page_view': 'Pageview',
  'purchase': 'CompletePayment',
  'search': 'Search',
  'sign_up': 'CompleteRegistration',
  'view_item': 'ViewContent',
};

const checkExistence = (key) => {
  return typeof copyFromWindow(key) !== "undefined";
};

const looksLikeSHA256 = (s) => {
  return s && s.length == 64;
};

const getConfigHash = (data) => {
  // What API is used. 0: None, 1: Tag Manager, 2: Google Tag, 3: Both
  var apiVersion = 0;
  if (ecommerce || userDataFromDataLayer) apiVersion += 1;
  if (eventModel && (eventModel.items || eventModel.value || eventModel.currency || eventModel.user_data)) apiVersion += 2;

  var contents = 0;
  if (data.enhance_ecomm == false && data.single_multi_product == "empty") contents = 1;
  else if (data.enhance_ecomm == false && data.single_multi_product == "single") contents = 2;
  else if (data.enhance_ecomm == false && data.single_multi_product == "multiple") contents = 3;
  else if (data.enhance_ecomm == true && data.ga_ecomm == "enhance_ecomm") contents = 4;
  else if (data.enhance_ecomm == true && data.ga_ecomm == "ecomm") contents = 5;

  return '' + apiVersion + contents;
};

const getEnhancedEcommerceData = (data, ecommerce) => {
  if (!ecommerce) {
    return null;
  }

  if (data.event == "ViewContent" && ecommerce.detail && ecommerce.detail.products) {
    return ecommerce.detail.products;
  } else if (data.event == "AddToCart" && ecommerce.add && ecommerce.add.products) {
    return ecommerce.add.products;
  } else if ((data.event == "InitiateCheckout" || data.event == "AddPaymentInfo") && ecommerce.checkout && ecommerce.checkout.products) {
    return ecommerce.checkout.products;
  } else if ((data.event == "CompletePayment" || data.event == "PlaceAnOrder" || data.event == "Subscribe") && ecommerce.purchase && ecommerce.purchase.products) {
    return ecommerce.purchase.products;
  } else {
    if (ecommerce.detail && ecommerce.detail.products)
      return ecommerce.detail.products;
    if (ecommerce.add && ecommerce.add.products)
      return ecommerce.add.products;
    if (ecommerce.checkout && ecommerce.checkout.products)
      return ecommerce.checkout.products;
    if (ecommerce.purchase && ecommerce.purchase.products)
      return ecommerce.purchase.products;
  }
  return null;
};

const getItemCategories = (item) => {
  var categories = [];
  if (item.item_category) categories.push(item.item_category);
  if (item.item_category2) categories.push(item.item_category2);
  if (item.item_category3) categories.push(item.item_category3);
  if (item.item_category4) categories.push(item.item_category4);
  if (item.item_category5) categories.push(item.item_category5);

  if (categories.length > 0) {
    return categories.join(',');
  }

  return null;
};

const getEcommerceData = (data, ecommerce) => {
  var value = 0;
  var currency = "USD"; // default value
  var contents = [];

  /* 
    Standard Ecommerce Support. For more details on Ecommerce Data Structure
    https://developers.google.com/analytics/devguides/collection/ga4/ecommerce?client_type=gtm
    We will need to check for standard ecommerce events if Enhanced Ecommerce doesnt work
  */
  const secData = (ecommerce && ecommerce.items) || (eventModel && eventModel.items) || ttContents;
  if (secData) {
    log("1a. Standard Ecommerce - Data layers detected");
    secData.map(function (ed) {
      // Form the TikTok Parameter Object
      var content = {};
      if (ed.item_id) content.content_id = ed.item_id;
      if (!ed.item_id && ed.id) content.content_id = ed.id;
      if (ed.item_name) content.content_name = ed.item_name;
      if (ed.item_brand) content.brand = ed.item_brand;
      var categoryString = getItemCategories(ed);
      if (categoryString) content.content_category = categoryString;
      if (ed.price) content.price = makeNumber(ed.price);
      if (ed.quantity) {
        content.quantity = makeNumber(ed.quantity);
      } else {
        content.quantity = 1;
      }
      content.content_type = ttContentType || "product";
      contents.push(content);
      // Calulate the total value shown
      if (ed.price) {
        if (!ed.quantity) ed.quantity = 1;
        value += ed.price * ed.quantity;
      }
    });
  } else if (data.ga_ecomm == "ecomm") {
    log("1a. Standard Ecommerce - Unable to detect any Data Layers");
  }

  /* 
    Enhanced Ecommerce Support. Fore more details on Enhanced Ecommerce Data Structure
    https://developers.google.com/analytics/devguides/collection/ua/gtm/enhanced-ecommerce#details
    ViewContent = detail
    AddToCart = add
    InitiateCheckout, AddPaymentInfo = checkout
    CompletePayment, PlaceAnOrder, Subscribe = purchase
  */
  const eecData = getEnhancedEcommerceData(data, ecommerce);
  if (eecData !== null && contents.length == 0) {
    log("1b. Enhanced Ecommerce - Data layers detected");
    eecData.map(function (ed) {
      // Form the TikTok Parameter Object
      var content = {};
      if (ed.id) content.content_id = ed.id;
      if (ed.name) content.content_name = ed.name;
      if (ed.brand) content.brand = ed.brand;
      if (ed.category) content.content_category = ed.category;
      if (ed.price) content.price = makeNumber(ed.price);
      if (ed.quantity) {
        content.quantity = makeNumber(ed.quantity);
      } else {
        content.quantity = 1;
      }
      content.content_type = ttContentType || "product";
      contents.push(content);
      // Calulate the total value shown
      if (ed.price) {
        if (!ed.quantity) ed.quantity = 1;
        value += ed.price * ed.quantity;
      }
    });
  } else if (data.ga_ecomm == "enhance_ecomm") {
    log("1b. Enhanced Ecommerce - Unable to detect any ecommerce data layers");
  }

  if (data.ecomm_currency)
    currency = data.ecomm_currency;
  else if (ecommerce && ecommerce.currencyCode)
    currency = ecommerce.currencyCode;
  else if (ecommerce && ecommerce.currency)
    currency = ecommerce.currency;
  else if (eventModel && eventModel.currency)
    currency = eventModel.currency;

  if (ecommerce && ecommerce.value) {
    value = ecommerce.value;
  } else if (
    ecommerce &&
    ecommerce.purchase &&
    ecommerce.purchase.actionField &&
    ecommerce.purchase.actionField.revenue
  ) {
    value = ecommerce.purchase.actionField.revenue;
  } else if (eventModel && eventModel.value) {
    value = eventModel.value;
  }

  return {
    value: value,
    currency: currency,
    contents: contents
  };
};

const isHashed = (val) => {
  return val && val.match("^[A-Fa-f0-9]{64}$") != null;
};

const getUserDataFromDataLayer = () => {
  var userData = {};
  const email = (userDataFromDataLayer && (userDataFromDataLayer.email_address || userDataFromDataLayer.sha256_email_address)) ||
    (eventModel && eventModel.user_data && (eventModel.user_data.email_address || eventModel.user_data.sha256_email_address));
  if (email) {
    if (isHashed(email)) {
      userData.sha256_email = email;
    } else {
      userData.email = email;
    }
  }

  const phone = (userDataFromDataLayer && (userDataFromDataLayer.phone_number || userDataFromDataLayer.sha256_phone_number)) ||
    (eventModel && eventModel.user_data && (eventModel.user_data.phone_number || eventModel.user_data.sha256_phone_number));
  if (phone) {
    if (isHashed(phone)) {
      userData.sha256_phone_number = phone;
    } else {
      userData.phone_number = phone;
    }
  }

  if (ttExternalId) {
    userData.external_id = ttExternalId;
  }

  return userData;
};

const main = () => {
  // Forming the passed in parameters
  var parameters = {
    gtm_version: version + ':' + getConfigHash(data),
    event_trigger_source: 'GoogleTagManagerClient',
  };
  const ttEvent = TTEventMap[data.event] || data.event;

  // Check if enhance ecomm is enabled.
  if (data.enhance_ecomm == true) {
    const ecomData = getEcommerceData(data, ecommerce);
    parameters.currency = ecomData.currency;
    parameters.value = ecomData.value;
    if (ecomData.contents && ecomData.contents.length > 0) {
      parameters.contents = ecomData.contents;
    }
  } else {
    if (data.single_multi_product == "single") {
      // Single Content
      if (data.content_id) parameters.content_id = data.content_id;
      if (data.content_type || ttContentType) parameters.content_type = data.content_type || ttContentType;
      if (data.content_name) parameters.content_name = data.content_name;
      if (data.price) parameters.price = makeNumber(data.price);
      if (data.quantity) parameters.quantity = makeNumber(data.quantity);
      if (data.currency) parameters.currency = data.currency;
      if (data.value) {
        parameters.value = makeNumber(data.value);
      } else {
        if (data.price && data.quantity) {
          parameters.value = makeNumber(data.price) * makeNumber(data.quantity);
        }
      }
      if (data.content_category)
        parameters.content_category = data.content_category;
      if (data.description) parameters.description = data.description;
      if (data.query) parameters.query = data.query;
      if (data.status) parameters.status = data.status;
    } else if (data.single_multi_product == "multiple") {
      // Multiple Content
      if (data.contents) {
        const parsedContents = JSON.parse(data.contents);
        if (parsedContents !== undefined) {
          parameters.contents = parsedContents;
        } else {
          parameters.contents = data.contents;
        }
      }
      if (data.currency) parameters.currency = data.currency;
      if (data.value) parameters.value = makeNumber(data.value);
      if (data.description) parameters.description = data.description;
      if (data.query) parameters.query = data.query;
      if (data.status) parameters.status = data.status;
    } else if (data.single_multi_product == "empty") {
      // No Content
      if (data.currency) parameters.currency = data.currency;
      if (data.value) parameters.value = makeNumber(data.value);
      if (data.description) parameters.description = data.description;
      if (data.query) parameters.query = data.query;
      if (data.status) parameters.status = data.status;
    }
  }

  // Additional Object Properties
  if (data.custom_properties && data.custom_properties.length > 0) {
    for (let i = 0; i < data.custom_properties.length; i++) {
      let objectParam = data.custom_properties[i];
      if (objectParam.key) {
        parameters[objectParam.key] = objectParam.value;
      }
    }
  }

  // Advanced Matching and sending TT events
  var userData = getUserDataFromDataLayer();

  if (data.hash == "hashed") {
    // Send hashed data
    if (data.sha256_email) userData.sha256_email = data.sha256_email;
    if (data.sha256_phone)
      userData.sha256_phone_number = data.sha256_phone;
    // Note: The key is still "external_id"
    if (data.sha256_external_id)
      userData.external_id = data.sha256_external_id;
    log("2a. Advanced matching with hashed PII");
  } else if (data.hash == "non-hashed") {
    // Send non-hashed data and TikTok Pixel will hash it for you.
    if (data.email) userData.email = data.email;
    if (data.phone) userData.phone_number = data.phone;
    if (data.external_id) userData.external_id = data.external_id;
    log(
      "2b. Advanced matching without hashed data, TikTok Pixel will hash the data for you."
    );
  }

  if (Object.keys(userData).length > 0) {
    callInWindow("ttq.identify", userData);
  }

  log(
    "3. Base code found, sending ",
    ttEvent,
    "event with",
    parameters,
    data.event_id
  );

  if (data.pixel_code) {
    callInWindow("ttq.track", ttEvent, parameters, {
      event_id: data.event_id,
      pixel_code: data.pixel_code
    });
  } else {
    callInWindow("ttq.track", ttEvent, parameters, {
      event_id: data.event_id
    });
  }
};

const validate = (data) => {
  const errors = [];
  const warnings = [];

  // errors
  if (!checkExistence("ttq")) {
    errors.push("ttq not found");
  }

  // clean up
  if (data.external_id) {
    data.external_id = data.external_id.trim();
  }

  // warnings
  // 1. Prevent user from passing in SHA256 hash as "email" or "phone" to avoid double-hashing.
  // 2. Prevent user from passing in non-hash as "sha256_email" or "sha256_phone".
  if (data.email) {
    if (data.email.indexOf("@") === -1)
      warnings.push("data.email is not an email");
    if (looksLikeSHA256(data.email))
      warnings.push("data.email appears to be a SHA256 hash");
  }
  if (data.phone) {
    if (looksLikeSHA256(data.phone))
      warnings.push("data.phone appears to be a SHA256 hash");
  }
  if (data.sha256_email) {
    if (!looksLikeSHA256(data.sha256_email))
      warnings.push("data.sha256_email is not a SHA256 hash");
  }
  if (data.sha256_phone) {
    if (!looksLikeSHA256(data.sha256_phone))
      warnings.push("data.sha256_phone is not a SHA256 hash");
  }

  if (TTEventMap[data.event] == undefined && ValidEvents[data.event] === undefined) {
    warnings.push('data.event "' + data.event + '" is not a valid event');
  }

  for (const msg of warnings) {
    log("[WARN] " + msg);
  }
  for (const msg of errors) {
    log("[ERROR] " + msg);
  }
  return errors;
};

const start = () => {
  const errors = validate(data);
  if (errors.length > 0) {
    data.gtmOnFailure();
    return;
  }

  main();

  data.gtmOnSuccess();
};

start();


___WEB_PERMISSIONS___

[
  {
    "instance": {
      "key": {
        "publicId": "logging",
        "versionId": "1"
      },
      "param": [
        {
          "key": "environments",
          "value": {
            "type": 1,
            "string": "debug"
          }
        }
      ]
    },
    "clientAnnotations": {
      "isEditedByUser": true
    },
    "isRequired": true
  },
  {
    "instance": {
      "key": {
        "publicId": "access_globals",
        "versionId": "1"
      },
      "param": [
        {
          "key": "keys",
          "value": {
            "type": 2,
            "listItem": [
              {
                "type": 3,
                "mapKey": [
                  {
                    "type": 1,
                    "string": "key"
                  },
                  {
                    "type": 1,
                    "string": "read"
                  },
                  {
                    "type": 1,
                    "string": "write"
                  },
                  {
                    "type": 1,
                    "string": "execute"
                  }
                ],
                "mapValue": [
                  {
                    "type": 1,
                    "string": "ttq"
                  },
                  {
                    "type": 8,
                    "boolean": true
                  },
                  {
                    "type": 8,
                    "boolean": false
                  },
                  {
                    "type": 8,
                    "boolean": false
                  }
                ]
              },
              {
                "type": 3,
                "mapKey": [
                  {
                    "type": 1,
                    "string": "key"
                  },
                  {
                    "type": 1,
                    "string": "read"
                  },
                  {
                    "type": 1,
                    "string": "write"
                  },
                  {
                    "type": 1,
                    "string": "execute"
                  }
                ],
                "mapValue": [
                  {
                    "type": 1,
                    "string": "ttq.identify"
                  },
                  {
                    "type": 8,
                    "boolean": false
                  },
                  {
                    "type": 8,
                    "boolean": false
                  },
                  {
                    "type": 8,
                    "boolean": true
                  }
                ]
              },
              {
                "type": 3,
                "mapKey": [
                  {
                    "type": 1,
                    "string": "key"
                  },
                  {
                    "type": 1,
                    "string": "read"
                  },
                  {
                    "type": 1,
                    "string": "write"
                  },
                  {
                    "type": 1,
                    "string": "execute"
                  }
                ],
                "mapValue": [
                  {
                    "type": 1,
                    "string": "ttq.track"
                  },
                  {
                    "type": 8,
                    "boolean": false
                  },
                  {
                    "type": 8,
                    "boolean": false
                  },
                  {
                    "type": 8,
                    "boolean": true
                  }
                ]
              }
            ]
          }
        }
      ]
    },
    "clientAnnotations": {
      "isEditedByUser": true
    },
    "isRequired": true
  },
  {
    "instance": {
      "key": {
        "publicId": "read_data_layer",
        "versionId": "1"
      },
      "param": [
        {
          "key": "allowedKeys",
          "value": {
            "type": 1,
            "string": "any"
          }
        }
      ]
    },
    "clientAnnotations": {
      "isEditedByUser": true
    },
    "isRequired": true
  }
]


___TESTS___

scenarios:
- name: Basic
  code: "const mockData = {\n  event: 'view_item',\n  pixel_code: 'my_pixel_code',\n\
    \  \n  advance_matching: true,\n  hash: \"non-hashed\",\n  external_id: '   abc\
    \   ',\n  \n  single_multi_product: 'single',\n  content_type: 'product',\n  content_id:\
    \ 'abc123',\n};\nrunCode(mockData);\n\nassertThat(Calls['ttq.identify'].length).isStrictlyEqualTo(1);\n\
    assertThat(Calls['ttq.identify'][0].params).isEqualTo({\n  \"external_id\": \"\
    abc\"\n});\n\nassertThat(Calls['ttq.track'].length).isStrictlyEqualTo(1);\nassertThat(Calls['ttq.track'][0].params.gtm_version).isEqualTo(\"\
    0_2_01:00\");\nassertThat(Calls['ttq.track'][0].params.content_type).isEqualTo(\"\
    product\");\nassertThat(Calls['ttq.track'][0].params.content_id).isEqualTo(\"\
    abc123\");\nassertThat(Calls['ttq.track'][0].params.event_trigger_source).isEqualTo(\"\
    GoogleTagManagerClient\");\nassertThat(Calls['ttq.track'][0].pixel.pixel_code).isEqualTo(\"\
    my_pixel_code\");\nassertThat(Calls['ttq.track'][0].eventName).isEqualTo(\"ViewContent\"\
    );\n\nassertApi('gtmOnSuccess').wasCalled();"
- name: MissingPixelCode
  code: |-
    const mockData = {
      event: 'ViewContent',
    };
    runCode(mockData);

    assertThat(LogsError.length).isStrictlyEqualTo(0);
    assertThat(Calls['ttq.track'][0].pixel.pixel_code).isEqualTo(undefined);
- name: InvalidPII
  code: "const mockData = {\n  event: 'CompletePayment',\n  pixel_code: 'my_pixel_code',\n\
    \  \n  email: 'ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad',\n\
    \  phone: 'ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad',\n\
    \  sha256_email: 'a@a.com',\n  sha256_phone: '+11231231234',\n};\nrunCode(mockData);\n\
    \n// Check hashed PII not passed into plain PII fields\nassertThat(LogsWarn).contains(\"\
    [WARN] data.email is not an email\");\nassertThat(LogsWarn).contains(\"[WARN]\
    \ data.email appears to be a SHA256 hash\");\nassertThat(LogsWarn).contains(\"\
    [WARN] data.phone appears to be a SHA256 hash\");\n\n// Check plain PII not passed\
    \ into hashed PII fields\nassertThat(LogsWarn).contains(\"[WARN] data.sha256_email\
    \ is not a SHA256 hash\");\nassertThat(LogsWarn).contains(\"[WARN] data.sha256_phone\
    \ is not a SHA256 hash\");\n\n"
- name: InvalidEvent
  code: |
    const mockData = {
      event: 'InvalidEvent',
      pixel_code: 'my_pixel_code',
    };
    runCode(mockData);

    // Check we warn about invalid event name
    assertThat(LogsWarn).contains('[WARN] data.event "InvalidEvent" is not a valid event');
- name: ValidPIIHashed
  code: "const mockData = {\n  event: 'CompletePayment',\n  pixel_code: 'my_pixel_code',\n\
    \  \n  hash: 'hashed',\n  sha256_email: 'ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad',\n\
    \  sha256_phone: 'ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad',\n\
    };\nrunCode(mockData);\n\nassertThat(Calls['ttq.identify'].length).isStrictlyEqualTo(1);\n\
    assertThat(Calls['ttq.identify'][0].params).isEqualTo({\n  \"sha256_email\": 'ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad',\n\
    \  \"sha256_phone_number\": 'ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad'\n\
    });\n"
- name: ValidPIIPlain
  code: "const mockData = {\n  event: 'CompletePayment',\n  pixel_code: 'my_pixel_code',\n\
    \  \n  hash: 'non-hashed',\n  email: 'a@a.com',\n  phone: '+11231231234',\n};\n\
    runCode(mockData);\n\nassertThat(Calls['ttq.identify'].length).isStrictlyEqualTo(1);\n\
    assertThat(Calls['ttq.identify'][0].params).isEqualTo({\n  \"email\": 'a@a.com',\n\
    \  \"phone_number\": '+11231231234'\n});\n\n"
- name: MultipleProducts
  code: |-
    const mockData = {
      event: 'CompletePayment',
      pixel_code: 'my_pixel_code',
      single_multi_product: 'multiple',
      contents: '[{"content_id": "abc123", "price": 1.23}]',
    };
    runCode(mockData);

    assertThat(Calls['ttq.track'].length).isStrictlyEqualTo(1);
    assertThat(Calls['ttq.track'][0].params.contents[0].content_id).isEqualTo("abc123");
    assertThat(Calls['ttq.track'][0].params.contents[0].price).isEqualTo(1.23);

    // Verify that the tag finished successfully.
    assertApi('gtmOnSuccess').wasCalled();
- name: MultipleProductsInvalidContentsJSON
  code: |-
    const mockData = {
      event: 'CompletePayment',
      event_id: '1234',
      pixel_code: 'my_pixel_code',
      single_multi_product: 'multiple',
      contents: '[{"content_id": "abc123", "price": 1.23}]]]]]',
    };
    runCode(mockData);

    assertThat(Calls['ttq.track'].length).isStrictlyEqualTo(1);
    // Parsing failure should just forward the event to Pixel.
    assertThat(Calls['ttq.track'][0].params.contents).isEqualTo('[{"content_id": "abc123", "price": 1.23}]]]]]');

    // Verify that the tag finished successfully.
    assertApi('gtmOnSuccess').wasCalled();
- name: SingleProduct
  code: |-
    const mockData = {
      event: 'CompletePayment',
      pixel_code: 'my_pixel_code',
      single_multi_product: 'single',
      content_id: 'abc123',
      content_type: 'product',
      content_name: 'tee shirt',
      price: '12.34',
      quantity: '2',
      currency: 'USD',
      value: '24.68',
      custom_properties: [
        {
          key: 'custom_property_1',
          value: 1,
        },
        {
          key: 'custom_property_2',
          value: '2',
        },
        {
          key: '',
          value: '3',
        },
      ],
    };
    runCode(mockData);

    assertThat(Calls['ttq.track'].length).isStrictlyEqualTo(1);
    assertThat(Calls['ttq.track'][0].params.content_id).isEqualTo("abc123");
    assertThat(Calls['ttq.track'][0].params.content_type).isEqualTo("product");
    assertThat(Calls['ttq.track'][0].params.content_name).isEqualTo("tee shirt");
    assertThat(Calls['ttq.track'][0].params.price).isEqualTo(12.34);
    assertThat(Calls['ttq.track'][0].params.quantity).isEqualTo(2);
    assertThat(Calls['ttq.track'][0].params.currency).isEqualTo("USD");
    assertThat(Calls['ttq.track'][0].params.value).isEqualTo(24.68);
    assertThat(Calls['ttq.track'][0].params.custom_property_1).isEqualTo(1);
    assertThat(Calls['ttq.track'][0].params.custom_property_2).isEqualTo('2');
    assertThat(Calls['ttq.track'][0].params['']).isUndefined();

    // Verify that the tag finished successfully.
    assertApi('gtmOnSuccess').wasCalled();
- name: StandardEcommerce
  code: |-
    const mockData = {
      event: 'CompletePayment',
      pixel_code: 'my_pixel_code',
      enhance_ecomm: true,
      ecommerce: {
        currency: 'CAD',
        value: 7.77,
        items: [
          {
              item_id: 'abc123',
              item_name: 'tee shirt',
              item_brand: 'mybrand',
              item_category: "Apparel",
              item_category2: "Adult",
              item_category4: "Crew",
              price: 9.99,
              discount: 2.22,
              quantity: 1
          }
        ]
      }
    };

    runCode(mockData);

    assertThat(Calls['ttq.track'].length).isStrictlyEqualTo(1);
    assertThat(Calls['ttq.track'][0].params.contents.length).isStrictlyEqualTo(1);
    assertThat(Calls['ttq.track'][0].params.contents[0].content_id).isEqualTo("abc123");
    assertThat(Calls['ttq.track'][0].params.contents[0].content_type).isEqualTo("product");
    assertThat(Calls['ttq.track'][0].params.contents[0].content_name).isEqualTo("tee shirt");
    assertThat(Calls['ttq.track'][0].params.contents[0].content_category).isEqualTo("Apparel,Adult,Crew");
    assertThat(Calls['ttq.track'][0].params.contents[0].brand).isEqualTo("mybrand");
    assertThat(Calls['ttq.track'][0].params.contents[0].price).isEqualTo(9.99);
    assertThat(Calls['ttq.track'][0].params.contents[0].quantity).isEqualTo(1);
    assertThat(Calls['ttq.track'][0].params.currency).isEqualTo("CAD");
    assertThat(Calls['ttq.track'][0].params.value).isEqualTo(7.77);

    assertApi('gtmOnSuccess').wasCalled();
- name: EnhancedEcommerce
  code: |-
    const mockData = {
      event: 'CompletePayment',
      pixel_code: 'my_pixel_code',
      enhance_ecomm: true,
      ecomm_currency: 'CNY',
      ecommerce: {
        detail: {
          products: [{
            name: 'tee shirt',
            id: 'abc123',
            price: '9.99',
            brand: 'mybrand',
            category: 'apparel',
            variant: 'blue'
           }]
         }
      }
    };

    runCode(mockData);

    assertThat(Calls['ttq.track'].length).isStrictlyEqualTo(1);
    assertThat(Calls['ttq.track'][0].params.contents.length).isStrictlyEqualTo(1);
    assertThat(Calls['ttq.track'][0].params.contents[0].content_id).isEqualTo("abc123");
    assertThat(Calls['ttq.track'][0].params.contents[0].content_type).isEqualTo("product");
    assertThat(Calls['ttq.track'][0].params.contents[0].content_name).isEqualTo("tee shirt");
    assertThat(Calls['ttq.track'][0].params.contents[0].content_category).isEqualTo("apparel");
    assertThat(Calls['ttq.track'][0].params.contents[0].brand).isEqualTo("mybrand");
    assertThat(Calls['ttq.track'][0].params.contents[0].price).isEqualTo(9.99);
    assertThat(Calls['ttq.track'][0].params.contents[0].quantity).isEqualTo(1);
    assertThat(Calls['ttq.track'][0].params.currency).isEqualTo("CNY");
    assertThat(Calls['ttq.track'][0].params.value).isEqualTo(9.99);

    assertApi('gtmOnSuccess').wasCalled();
setup: "const log = require('logToConsole');\n\nconst LogsError = [];\nconst LogsWarn\
  \ = []; \nconst Calls = {};\n\nmock('logToConsole', function() {\n  if (arguments.length\
  \ > 0 && arguments[0].substring(0, 7) == '[ERROR]') {\n    LogsError.push(arguments[0]);\n\
  \  }\n  if (arguments.length > 0 && arguments[0].substring(0, 6) == '[WARN]') {\n\
  \    LogsWarn.push(arguments[0]);\n  }\n  // Print out logs, as normally as possible\n\
  \  log(arguments); // Note: bind and call not available\n});\n\nmock('callInWindow',\
  \ function() {\n  const fnName = arguments[0];\n  Calls[fnName] = Calls[fnName]\
  \ || [];\n  const call = {};\n  call.arguments = arguments;\n  Calls[fnName].push(call);\n\
  \  \n  if (fnName === 'ttq.identify') {\n    call.params = arguments[1];\n  } else\
  \ if (fnName === 'ttq.track') {\n    call.eventName = arguments[1];\n    call.params\
  \ = arguments[2];\n    call.pixel = arguments[3];\n  }\n});\n\nmock('copyFromWindow',\
  \ function(name) {\n  // Assume base code always exists.\n  return {};\n});\n\n\
  mock('copyFromDataLayer', function(name) {\n  if (name == \"ecommerce\") {\n   \
  \ return mockData.ecommerce;\n  }\n  \n  return undefined;\n});"


___NOTES___

Created on 19/08/2022, 17:04:57


